/*!
 * Should
 * Copyright(c) 2010-2012 TJ Holowaychuk <tj@vision-media.ca>
 * MIT Licensed
 */

/**
 * Module dependencies.
 */

var util = require('util')
    , http = require('http')
    , assert = require('assert')
    , AssertionError = assert.AssertionError
    , statusCodes = http.STATUS_CODES
    , eql = require('./eql')
    , i = util.inspect;

/**
 * Expose assert as should.
 *
 * This allows you to do things like below
 * without require()ing the assert module.
 *
 *    should.equal(foo.bar, undefined);
 *
 */

exports = module.exports = assert;

/**
 * Assert _obj_ exists, with optional message.
 *
 * @param {Mixed} obj
 * @param {String} [msg]
 * @api public
 */

exports.exist = exports.exists = function(obj, msg){
    if (null == obj) {
        throw new AssertionError({
            message: msg || ('expected ' + i(obj) + ' to exist')
            , stackStartFunction: exports.exist
        });
    }
};

/**
 * Asserts _obj_ does not exist, with optional message.
 *
 * @param {Mixed} obj
 * @param {String} [msg]
 * @api public
 */

exports.not = {};
exports.not.exist = exports.not.exists = function(obj, msg){
    if (null != obj) {
        throw new AssertionError({
            message: msg || ('expected ' + i(obj) + ' to not exist')
            , stackStartFunction: exports.not.exist
        });
    }
};

/**
 * Expose api via `Object#should`.
 *
 * @api public
 */

Object.defineProperty(Object.prototype, 'should', {
    set: function(){},
    get: function(){
        return new Assertion(Object(this).valueOf());
    },
    configurable: true
});

/**
 * Initialize a new `Assertion` with the given _obj_.
 *
 * @param {Mixed} obj
 * @api private
 */

var Assertion = exports.Assertion = function Assertion(obj) {
    this.obj = obj;
};

/**
 * Prototype.
 */

Assertion.prototype = {

    /**
     * HACK: prevents double require() from failing.
     */

    exports: exports,

    /**
     * Assert _expr_ with the given _msg_ and _negatedMsg_.
     *
     * @param {Boolean} expr
     * @param {String} msg
     * @param {String} negatedMsg
     * @param {Object} expected
     * @api private
     */

    assert: function(expr, msg, negatedMsg, expected, showDiff){
        var msg = this.negate ? negatedMsg : msg
            , ok = this.negate ? !expr : expr
            , obj = this.obj;

        if (ok) return;

        var err = new AssertionError({
            message: msg.call(this)
            , actual: obj
            , expected: expected
            , stackStartFunction: this.assert
            , negated: this.negate
        });

        err.showDiff = showDiff;

        throw err;
    },

    /**
     * Dummy getter.
     *
     * @api public
     */

    get an() {
        return this;
    },

    /**
     * Dummy getter.
     *
     * @api public
     */

    get and() {
        return this;
    },

    /**
     * Dummy getter.
     *
     * @api public
     */

    get be() {
        return this;
    },

    /**
     * Dummy getter.
     *
     * @api public
     */

    get have() {
        return this;
    },

    /**
     * Dummy getter.
     *
     * @api public
     */

    get with() {
        return this;
    },

    /**
     * Negation modifier.
     *
     * @api public
     */

    get not() {
        this.negate = true;
        return this;
    },

    /**
     * Get object inspection string.
     *
     * @return {String}
     * @api private
     */

    get inspect() {
        return i(this.obj);
    },

    /**
     * Assert instanceof `Arguments`.
     *
     * @api public
     */

    get arguments() {
        this.assert(
            '[object Arguments]' == Object.prototype.toString.call(this.obj)
            , function(){ return 'expected ' + this.inspect + ' to be arguments' }
            , function(){ return 'expected ' + this.inspect + ' to not be arguments' });
        return this;
    },

    /**
     * Assert that an object is empty aka length of 0.
     *
     * @api public
     */

    get empty() {
        this.obj.should.have.property('length');
        this.assert(
            0 === this.obj.length
            , function(){ return 'expected ' + this.inspect + ' to be empty' }
            , function(){ return 'expected ' + this.inspect + ' not to be empty' });
        return this;
    },

    /**
     * Assert ok.
     *
     * @api public
     */

    get ok() {
        this.assert(
            this.obj
            , function(){ return 'expected ' + this.inspect + ' to be truthy' }
            , function(){ return 'expected ' + this.inspect + ' to be falsey' });
        return this;
    },

    /**
     * Assert true.
     *
     * @api public
     */

    get true() {
        this.assert(
            true === this.obj
            , function(){ return 'expected ' + this.inspect + ' to be true' }
            , function(){ return 'expected ' + this.inspect + ' not to be true' });
        return this;
    },

    /**
     * Assert false.
     *
     * @api public
     */

    get false() {
        this.assert(
            false === this.obj
            , function(){ return 'expected ' + this.inspect + ' to be false' }
            , function(){ return 'expected ' + this.inspect + ' not to be false' });
        return this;
    },

    /**
     * Assert equal.
     *
     * @param {Mixed} val
     * @param {String} description
     * @api public
     */

    eql: function(val, desc){
        this.assert(
            eql(val, this.obj)
            , function(){ return 'expected ' + this.inspect + ' to equal ' + i(val) + (desc ? " | " + desc : "") }
            , function(){ return 'expected ' + this.inspect + ' to not equal ' + i(val) + (desc ? " | " + desc : "") }
            , val
            , true);
        return this;
    },

    /**
     * Assert strict equal.
     *
     * @param {Mixed} val
     * @param {String} description
     * @api public
     */

    equal: function(val, desc){
        this.assert(
            val.valueOf() === this.obj
            , function(){ return 'expected ' + this.inspect + ' to equal ' + i(val) + (desc ? " | " + desc : "") }
            , function(){ return 'expected ' + this.inspect + ' to not equal ' + i(val) + (desc ? " | " + desc : "") }
            , val);
        return this;
    },

    /**
     * Assert within start to finish (inclusive).
     *
     * @param {Number} start
     * @param {Number} finish
     * @param {String} description
     * @api public
     */

    within: function(start, finish, desc){
        var range = start + '..' + finish;
        this.assert(
            this.obj >= start && this.obj <= finish
            , function(){ return 'expected ' + this.inspect + ' to be within ' + range + (desc ? " | " + desc : "") }
            , function(){ return 'expected ' + this.inspect + ' to not be within ' + range + (desc ? " | " + desc : "") });
        return this;
    },

    /**
     * Assert within value +- delta (inclusive).
     *
     * @param {Number} value
     * @param {Number} delta
     * @param {String} description
     * @api public
     */

    approximately: function(value, delta, description) {
        return this.within(value - delta, value + delta, description);
    },

    /**
     * Assert typeof.
     *
     * @param {Mixed} type
     * @param {String} description
     * @api public
     */

    a: function(type, desc){
        this.assert(
            type == typeof this.obj
            , function(){ return 'expected ' + this.inspect + ' to be a ' + type + (desc ? " | " + desc : "") }
            , function(){ return 'expected ' + this.inspect + ' not to be a ' + type  + (desc ? " | " + desc : "") })
        return this;
    },

    /**
     * Assert instanceof.
     *
     * @param {Function} constructor
     * @param {String} description
     * @api public
     */

    instanceof: function(constructor, desc){
        var name = constructor.name;
        this.assert(
            this.obj instanceof constructor
            , function(){ return 'expected ' + this.inspect + ' to be an instance of ' + name + (desc ? " | " + desc : "") }
            , function(){ return 'expected ' + this.inspect + ' not to be an instance of ' + name + (desc ? " | " + desc : "") });
        return this;
    },

    /**
     * Assert numeric value above _n_.
     *
     * @param {Number} n
     * @param {String} description
     * @api public
     */

    above: function(n, desc){
        this.assert(
            this.obj > n
            , function(){ return 'expected ' + this.inspect + ' to be above ' + n + (desc ? " | " + desc : "") }
            , function(){ return 'expected ' + this.inspect + ' to be below ' + n + (desc ? " | " + desc : "") });
        return this;
    },

    /**
     * Assert numeric value below _n_.
     *
     * @param {Number} n
     * @param {String} description
     * @api public
     */

    below: function(n, desc){
        this.assert(
            this.obj < n
            , function(){ return 'expected ' + this.inspect + ' to be below ' + n + (desc ? " | " + desc : "") }
            , function(){ return 'expected ' + this.inspect + ' to be above ' + n + (desc ? " | " + desc : "") });
        return this;
    },

    /**
     * Assert string value matches _regexp_.
     *
     * @param {RegExp} regexp
     * @param {String} description
     * @api public
     */

    match: function(regexp, desc){
        this.assert(
            regexp.exec(this.obj)
            , function(){ return 'expected ' + this.inspect + ' to match ' + regexp + (desc ? " | " + desc : "") }
            , function(){ return 'expected ' + this.inspect + ' not to match ' + regexp + (desc ? " | " + desc : "") });
        return this;
    },

    /**
     * Assert property "length" exists and has value of _n_.
     *
     * @param {Number} n
     * @param {String} description
     * @api public
     */

    length: function(n, desc){
        this.obj.should.have.property('length');
        var len = this.obj.length;
        this.assert(
            n == len
            , function(){ return 'expected ' + this.inspect + ' to have a length of ' + n + ' but got ' + len + (desc ? " | " + desc : "") }
            , function(){ return 'expected ' + this.inspect + ' to not have a length of ' + len + (desc ? " | " + desc : "") });
        return this;
    },

    /**
     * Assert property _name_ exists, with optional _val_.
     *
     * @param {String} name
     * @param {Mixed} [val]
     * @param {String} description
     * @api public
     */

    property: function(name, val, desc){
        if (this.negate && undefined !== val) {
            if (undefined === this.obj[name]) {
                throw new Error(this.inspect + ' has no property ' + i(name) + (desc ? " | " + desc : ""));
            }
        } else {
            this.assert(
                undefined !== this.obj[name]
                , function(){ return 'expected ' + this.inspect + ' to have a property ' + i(name) + (desc ? " | " + desc : "") }
                , function(){ return 'expected ' + this.inspect + ' to not have a property ' + i(name) + (desc ? " | " + desc : "") });
        }

        if (undefined !== val) {
            this.assert(
                val === this.obj[name]
                , function(){ return 'expected ' + this.inspect + ' to have a property ' + i(name)
                    + ' of ' + i(val) + ', but got ' + i(this.obj[name]) + (desc ? " | " + desc : "") }
                , function(){ return 'expected ' + this.inspect + ' to not have a property ' + i(name) + ' of ' + i(val) + (desc ? " | " + desc : "") });
        }

        this.obj = this.obj[name];
        return this;
    },

    /**
     * Assert own property _name_ exists.
     *
     * @param {String} name
     * @param {String} description
     * @api public
     */

    ownProperty: function(name, desc){
        this.assert(
            this.obj.hasOwnProperty(name)
            , function(){ return 'expected ' + this.inspect + ' to have own property ' + i(name) + (desc ? " | " + desc : "") }
            , function(){ return 'expected ' + this.inspect + ' to not have own property ' + i(name) + (desc ? " | " + desc : "") });
        this.obj = this.obj[name];
        return this;
    },

    /**
     * Assert that `obj` is present via `.indexOf()`.
     *
     * @param {Mixed} obj
     * @param {String} description
     * @api public
     */

    include: function(obj, desc){
        if (obj.constructor == Object){
            var cmp = {};
            for (var key in obj) cmp[key] = this.obj[key];
            this.assert(
                eql(cmp, obj)
                , function(){ return 'expected ' + this.inspect + ' to include an object equal to ' + i(obj) + (desc ? " | " + desc : "") }
                , function(){ return 'expected ' + this.inspect + ' to not include an object equal to ' + i(obj) + (desc ? " | " + desc : "") });
        } else {
            this.assert(
                ~this.obj.indexOf(obj)
                , function(){ return 'expected ' + this.inspect + ' to include ' + i(obj) + (desc ? " | " + desc : "") }
                , function(){ return 'expected ' + this.inspect + ' to not include ' + i(obj) + (desc ? " | " + desc : "") });
        }
        return this;
    },

    /**
     * Assert that an object equal to `obj` is present.
     *
     * @param {Array} obj
     * @param {String} description
     * @api public
     */

    includeEql: function(obj, desc){
        this.assert(
            this.obj.some(function(item) { return eql(obj, item); })
            , function(){ return 'expected ' + this.inspect + ' to include an object equal to ' + i(obj) + (desc ? " | " + desc : "") }
            , function(){ return 'expected ' + this.inspect + ' to not include an object equal to ' + i(obj) + (desc ? " | " + desc : "") });
        return this;
    },

    /**
     * Assert that the array contains _obj_.
     *
     * @param {Mixed} obj
     * @api public
     */

    contain: function(obj){
        console.warn('should.contain() is deprecated, use should.include()');
        this.obj.should.be.an.instanceof(Array);
        this.assert(
            ~this.obj.indexOf(obj)
            , function(){ return 'expected ' + this.inspect + ' to contain ' + i(obj) }
            , function(){ return 'expected ' + this.inspect + ' to not contain ' + i(obj) });
        return this;
    },

    /**
     * Assert exact keys or inclusion of keys by using
     * the `.include` modifier.
     *
     * @param {Array|String ...} keys
     * @api public
     */

    keys: function(keys){
        var str
            , ok = true;

        keys = keys instanceof Array
            ? keys
            : Array.prototype.slice.call(arguments);

        if (!keys.length) throw new Error('keys required');

        var actual = Object.keys(this.obj)
            , len = keys.length;

        // make sure they're all present
        ok = keys.every(function(key){
            return ~actual.indexOf(key);
        });

        // matching length
        ok = ok && keys.length == actual.length;

        // key string
        if (len > 1) {
            keys = keys.map(function(key){
                return i(key);
            });
            var last = keys.pop();
            str = keys.join(', ') + ', and ' + last;
        } else {
            str = i(keys[0]);
        }

        // message
        str = 'have ' + (len > 1 ? 'keys ' : 'key ') + str;

        this.assert(
            ok
            , function(){ return 'expected ' + this.inspect + ' to ' + str }
            , function(){ return 'expected ' + this.inspect + ' to not ' + str });

        return this;
    },

    /**
     * Assert that header `field` has the given `val`.
     *
     * @param {String} field
     * @param {String} val
     * @return {Assertion} for chaining
     * @api public
     */

    header: function(field, val){
        this.obj.should
            .have.property('headers').and
            .have.property(field.toLowerCase(), val);
        return this;
    },

    /**
     * Assert `.statusCode` of `code`.
     *
     * @param {Number} code
     * @return {Assertion} for chaining
     * @api public
     */

    status:  function(code){
        this.obj.should.have.property('statusCode');
        var status = this.obj.statusCode;

        this.assert(
            code == status
            , function(){ return 'expected response code of ' + code + ' ' + i(statusCodes[code])
                + ', but got ' + status + ' ' + i(statusCodes[status]) }
            , function(){ return 'expected to not respond with ' + code + ' ' + i(statusCodes[code]) });

        return this;
    },

    /**
     * Assert that this response has content-type: application/json.
     *
     * @return {Assertion} for chaining
     * @api public
     */

    get json() {
        this.obj.should.have.property('headers');
        this.obj.headers.should.have.property('content-type');
        this.obj.headers['content-type'].should.include('application/json');
        return this;
    },

    /**
     * Assert that this response has content-type: text/html.
     *
     * @return {Assertion} for chaining
     * @api public
     */

    get html() {
        this.obj.should.have.property('headers');
        this.obj.headers.should.have.property('content-type');
        this.obj.headers['content-type'].should.include('text/html');
        return this;
    },

    /**
     * Assert that this function will or will not
     * throw an exception.
     *
     * @return {Assertion} for chaining
     * @api public
     */

    throw: function(message){
        var fn = this.obj
            , err = {}
            , errorInfo = ''
            , ok = true;

        try {
            fn();
            ok = false;
        } catch (e) {
            err = e;
        }

        if (ok) {
            if ('string' == typeof message) {
                ok = message == err.message;
            } else if (message instanceof RegExp) {
                ok = message.test(err.message);
            } else if ('function' == typeof message) {
                ok = err instanceof message;
            }

            if (message && !ok) {
                if ('string' == typeof message) {
                    errorInfo = " with a message matching '" + message + "', but got '" + err.message + "'";
                } else if (message instanceof RegExp) {
                    errorInfo = " with a message matching " + message + ", but got '" + err.message + "'";
                } else if ('function' == typeof message) {
                    errorInfo = " of type " + message.name + ", but got " + err.constructor.name;
                }
            }
        }

        this.assert(
            ok
            , function(){ return 'expected an exception to be thrown' + errorInfo }
            , function(){ return 'expected no exception to be thrown, got "' + err.message + '"' });

        return this;
    }
};

/**
 * Aliases.
 */

(function alias(name, as){
    Assertion.prototype[as] = Assertion.prototype[name];
    return alias;
})
    ('instanceof', 'instanceOf')
    ('throw', 'throwError')
    ('length', 'lengthOf')
    ('keys', 'key')
    ('ownProperty', 'haveOwnProperty')
    ('above', 'greaterThan')
    ('below', 'lessThan');
